/*
 * Copyright 2009 Gordon Turner
 * Copyright 2008 Garth Shoemaker
 * 
 * This file is part of openwiimote.
 *
 * openwiimote is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * openwiimote is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public License
 * along with Wiimote Simple.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.openwiimote;

import java.util.LinkedList;
import java.util.ListIterator;

import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DeviceClass;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.DiscoveryListener;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;

import org.apache.log4j.Logger;

/**
 * This class is used to perform searches for wiimotes. When it finds a wiimote
 * it creates a Wiimote instance and notifies any listeners.
 */
public class WiimoteDiscoverer extends Thread implements DiscoveryListener
{
  Logger logger = Logger.getLogger( WiimoteDiscoverer.class );

  static LinkedList discoveredWiimoteAddresses = new LinkedList();

  static WiimoteDiscoverer discoverer;

  String discoveredAddress;

  DiscoveryAgent agent;

  Object lock;

  LinkedList listeners;

  protected WiimoteDiscoverer()
  {
    listeners = new LinkedList();

    try
    {
      LocalDevice device = LocalDevice.getLocalDevice();
      agent = device.getDiscoveryAgent();
      lock = new Object();
    }
    catch( BluetoothStateException e )
    {
      logger.debug( "BluetoothStateException. Exiting. " + e );
      System.exit( 0 );
    }
    catch( Exception e )
    {
      logger.debug( "exception " + e );
    }
  }

  /**
   * Adds a WiimoteDiscoveryListener to the list of listeners. This listener
   * will be notified whenver a Wiimote is discovered and connected to.
   */
  public void addWiimoteDiscoveryListener( WiimoteDiscoveryListener l )
  {
    if( !listeners.contains( l ) )
    {
      listeners.add( l );
    }
  }

  /**
   * Removes the given WiimoteDiscoveryListener from the list of listeners.
   */
  public void removeWiimoteDiscoveryListener( WiimoteDiscoveryListener l )
  {
    listeners.remove( l );
  }

  /**
   * Returns an instance of WiimoteDiscoverer. This is how you should get one 
   * for use.
   */
  synchronized static public WiimoteDiscoverer getWiimoteDiscoverer()
  {
    if( discoverer == null )
    {
      discoverer = new WiimoteDiscoverer();
    }

    return discoverer;
  }

  /**
   * Notifies listeners that a wiimote has been discovered and connected to.
   */
  protected void notifyListeners( Wiimote wiimote )
  {
    logger.warn( "BEGIN" );
    logger.debug( Thread.currentThread().getName() );

    for( ListIterator it = listeners.listIterator(); it.hasNext(); )
    {
      logger.debug( "Attempting to call "
          + "WiimoteDiscoveryListener.wiimoteDiscovered()" );
      ( (WiimoteDiscoveryListener) it.next() ).wiimoteDiscovered( wiimote );
    }

    logger.warn( "END" );
  }

  /**
   * This runs while wiimotes are being searched for. It is initiated by a 
   * call to startWiimoteSearch().  It continusously finds wiimotes and 
   * notifies any listeners until stopWiimoteSearch() is called.
   */
  public void run()
  {
    logger.warn( "BEGIN" );

    System.out.println( "Press 1 & 2 on your Wiimote now" );
    System.out.println( "If wiimote is already connected, press and hold "
        + "the power button to disconnect first." );
    logger.debug( "Press 1 & 2 on your Wiimote now" );

    Wiimote wiimote = null;
    try
    {
      while( true && !isInterrupted() )
      {
        while( discoveredAddress == null )
        {
          logger.warn( "agent.startInquiry() called" );

          agent.startInquiry( DiscoveryAgent.GIAC, this );
          synchronized( lock )
          {
            lock.wait();
          }
        }
        ;

        try
        {
          if( discoveredAddress != null )
          {
            wiimote = new Wiimote( discoveredAddress );
            System.out.println( "Connected to new wiimote." );
            logger.debug( "Connected to new wiimote." );
            notifyListeners( wiimote );
            wiimote = null;
            discoveredWiimoteAddresses.add( discoveredAddress );
            discoveredAddress = null;
          }
        }
        catch( Exception e )
        {
          discoveredAddress = null;
          discoveredWiimoteAddresses.remove( discoveredAddress );
          logger.debug( "Connection failed. Try again. " + e );
        }
      }
    }
    catch( InterruptedException e )
    {
      logger.debug( "InterruptedException thrown" );
    }
    catch( BluetoothStateException bse )
    {
      /*
       *  For some reason, OSX always throws this exception and yet still works.
       *  
       *  So just catch the error for now, and ignore it.
       */
    }

    logger.warn( "END" );
  }

  /**
   * Starts a search for Wiimotes. The search will continue until
   * stopWiimoteSearch is called.
   */
  public void startWiimoteSearch()
  {
    if( !isAlive() )
    {
      start();
    }
  }

  /**
   * Stops a search for Wiimotes. Call this if you are done connecting with 
   * wiimotes.
   */
  public void stopWiimoteSearch()
  {
    if( isAlive() )
    {
      interrupt();
    }
  }

  /**
   * Is called by a JSR-82 implementation when a Bluetooth inquiry is complete. 
   * This occurs either after a Wiimote has been found, or after the Bluetooth 
   * implementation has given up. It should not be necessary to call this.
   */
  public void inquiryCompleted( int i )
  {
    logger.warn( "JSR-82 Callback: BEGIN" );
    synchronized( lock )
    {
      lock.notifyAll();
    }
    logger.warn( "JSR-82 Callback: END" );
  }

  /**
   * Is called by a JSR-82 implementation when a device has been discovered. 
   * It should not be necessary to call this.
   */
  public void deviceDiscovered( RemoteDevice remotedevice,
      DeviceClass deviceclass )
  {
    logger.warn( "JSR-82 Callback: BEGIN" );

    String name = null;
    try
    {
      name = remotedevice.getFriendlyName( false );
      logger.debug( "JSR-82 Callback: Discovered '" + name + "'" );
    }
    catch( Exception e )
    {
      logger.debug( e );
    }

    // The name must be "Nintendo RVL-CNT-01", or it isn't a wiimote.
    if( !name.equals( "Nintendo RVL-CNT-01" ) )
    {
      logger.debug( "JSR-82 Callback: Not a wiimote." );
      return;
    }
    else
    {
      logger.debug( "JSR-82 Callback: Found a wiimote." );

      // Found a wiimote, get it's address.
      String address = remotedevice.getBluetoothAddress();
      logger.debug( "JSR-82 Callback: " + address );

      // Return if it isn't a new wiimote.
      if( discoveredWiimoteAddresses.contains( address ) )
      {
        logger.debug( "JSR-82 Callback: Already connected." );
      }
      else
      {
        discoveredAddress = address;
      }
    }

    agent.cancelInquiry( this );

    logger.warn( "JSR-82 Callback: END" );
  }

  /**
   * Not implemented
   */
  public void servicesDiscovered( int i, ServiceRecord aservicerecord[] )
  {
    logger.warn( "JSR-82 Callback: BEGIN" );
    logger.warn( "JSR-82 Callback: END" );
  }

  /**
   * Not implemented
   */
  public void serviceSearchCompleted( int i, int j )
  {
    logger.warn( "JSR-82 Callback: BEGIN" );
    logger.warn( "JSR-82 Callback: END" );
  }

}
